<?php

/**
 * CryoBB
 *
 * CryoBB (create your own bulletin board) is a library that enables developers
 * to create and parse their own defined BB Code tags - without the need to 
 * create a complex parser or write complex functions in their code base when 
 * adding or modifying BB Code tags.
 * 
 * With CryoBB, the developer simply defines what tags to use, what emoticons 
 * to use, and how they should behave.
 *
 * CryoBB can be used to create a fully functional powerful BB Code parsing 
 * engine.. including most commonly used BB Code tags, cool tags found on other 
 * custom BB Code systems, and also to create never-before-seen custom tags.
 *
 * Features
 * ----------------------------------------------------------------------------
 * Create tags and define their HTML replacements and how they behave
 * Simple tags.. e.g. [b] [u] [i] [img]
 * Tags with parameters, even optional parameters!  e.g. 
 *   [quote="name"]text[/quote] or [quote]text[/quote]
 * Define your own placeholder macro in your HTML replacements
 * Use text between tags as a parameter, such as 
 *   [url]http://www.example.com[/url]
 * Create tags that do not have their contents parsed, such as 
 *   [code]text[/code] or [text]text[/text]
 * Apply built-in or user defined functions to tags parameters and contents 
 *   (highlight code syntax or do something really cool?)
 * Apply built-in or user defined functions to a tags contents based on
 *   the parameter supplied
 * Turns new lines into HTML line breaks
 * Because of full-tag matching and replacing, your entire page will not be 
 *   riddled with ridiculous formatting
 * Correctly parses nested bb code tags of the same kind (e.g. nested quote 
 *   tags)
 * Recursively parses inside of bb code tags to correctly parse nested tags
 *   (e.g. [b][u][i]text[/i][/u][/b])
 * Naming convention complies with standards for autoloading interoperability 
 *   (http://groups.google.com/group/php-standards/web/psr-0-final-proposal?pli=1)
 * ----------------------------------------------------------------------------
 *
 *
 * @Todo
 * ----------------------------------------------------------------------------
 * Improve regular expressions
 * Create a hyperlinking function that turns non bb coded URLs into clickable 
 *    hyperlinks.
 * Better error reporting and verbosity
 * ----------------------------------------------------------------------------
 *
 *
 * Version History
 * ----------------------------------------------------------------------------
 * 1.0.0 - Initial Release
 * ----------------------------------------------------------------------------
 *
 * @package CryoBB
 * @filename Base.php
 * @version 1.0.0
 * @author Scott Martin <sjm.dev1[at]gmail[dot]com>
 * @date November 5th, 2010
 * @license GNU GPL V3
 
 <CryoBB>
    Copyright (C) <2010>  <Scott Martin>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    
 */

class CryoBB_Base
{
	//the regex template
	const REGEX_TEMPLATE = '/
\[								# tag opening
(%s)							# tag name
(								# optional parameter
	=(
		(?:
			(?R)
			|[^[]
			|\[(?!
				\/?\\1(=.+?)?\]
			)
		)+?						# occuring lots of times
	)
)?								# parameter optional
\]								# tag closing
(
	(?:
		[^[]
		|\[(?!
			\/?\\1(=.+?)?\]
		)
		|(?R)
	)+
)
\[\/\\1\]						# end of tag
/six';
	
	//the tags and definitions supplied by dev
	private $tags = array();
	
	//the emoticons supplied by dev
	private $emoticons = array();
	
	//the regexp for matching tags
	private $matchRegexp;
	
	/**
	 * Converts input string to HTML
	 *
	 * According to parameters supplied, parses input string containing bb code 
	 * and emoticons into their HTML equivelants.
	 *
	 * @access public
	 * @param string $str - the input string to be parsed for bb code and emoticons
	 * @param boolean $parseBB - true to parse bb code, false not to
	 * @param boolean $parseEmoticons - true to parse emoticons, false not to
	 * @param isXHTML - true to use <br /> line breaks, false to use <br> (php >= 5.3.0)
	 * @access public
	 * @return string - input string converted to HTML according to parameters
	 */
	public function make($str, $parseBB=true, $parseEmoticons=true, $isXHTML=true)
	{
		
		//don't parse any tags if we don't have to
		if ($parseBB && !empty($this->tags) && (strpos($str, '[') !== false) && (strpos($str, ']') !== false))
		{
			//make regexp's for tags
			$this->matchRegexp		= $this->makeRegexps();
			
			//make the tags that do need parsed
			$replaceMatches	= $this->matchTags($str);
			$str = $this->makeTags($str, $replaceMatches);
		}

		//parse emoticons
		if ($parseEmoticons)
		{
			$str = $this->makeEmoticons($str);
		}
		
		//make line breaks
		$str = $this->makeLineBreaks($str, $isXHTML);
		
		//and return this thing of beauty we have created
		return $str;
	}
	
	/**
	 * Makes regular expressions
	 *
	 * Makes regular expressions based on the tags supplied via the addTags()
	 * method.  Based on the value of parameter $extract, a regular expression is 
	 * returned to match all tags or the tags to be extracted from the input 
	 * string.  Called from public method make()
	 *
	 * @access private
	 * @return mixed - string regexp when tag list is not empty and/or extraction 
	 *  tags can be found, false if otherwise
	 */
	private function makeRegexps()
	{
		$tagList	= array_keys($this->tags);
		$regexp		= '';

		foreach ($tagList AS $tag)
		{
			$regexp .= '|' . preg_quote($tag);
		}

		return (!empty($regexp) === true)
			? sprintf(self::REGEX_TEMPLATE, $regexp)
			: false;
	}
	
    /**
     * Matches bb code tags and their contents
     *
     * Recursively matches bb code tags and their contents based on the regexp 
     * in property $matchRegexp.  Recursion is necessary to capture nested bb 
     * code elements.  Stores matches in an array and returns them.
     *
     * @param $str - the string to match for bb code tags
     * @param $offset - the offset of the parent tag
     * @access private
     * @return mixed - array of matches if found, false if no matches found
     */
	private function matchTags($str, $offset = 0)
	{
		if ((strpos($str, '[') !== false) && (strpos($str, ']') !== false))
		{
			if (preg_match_all($this->matchRegexp, $str, $matchesReference, PREG_SET_ORDER | PREG_OFFSET_CAPTURE))
			{
				$matches = array();
				
				foreach ($matchesReference as $match)
				{
					$match = new CryoBB_Match($match, $offset);
					$matches[] = $match;
					
					$newMatches = $this->matchTags($match->content, $match->childOffset());
					if ($newMatches)
					{
						$matches = array_merge($matches, $newMatches);
					}
				}
				return $matches;
			}
		}

		return array();
	}
	
	
	/**
	 * Adds a bb code tag or multiple bb code tags to this bbcode ojbect
	 *
	 * Adds tag definitions to $this->tags properties.  Multiple tag definitions 
	 * can be added at once.  Checks to make sure tags are instances of CryoBB_Tag
	 *
	 * @param mixed $tags Array of tags, or many arguments
	 * @access public
	 */
	public function addTags($tags = null)
	{
		if (!is_array($tags)) {
			$tags = func_get_args();
		}
		
		foreach ($tags AS $tag)
		{
			$this->addTag($tag);
		}
		return $this;
	}
	
	/**
	 * Alias of addTags
	 * 
	 * @param CryoBB_Tag $tag Tag to add
	 */
	public function addTag(CryoBB_Tag $tag)
	{
		$this->tags[strtolower($tag->tag)] = $tag;
		return $this;
	}
	
	/**
	 * Adds emoticon definitions to this object.
	 *
	 * @param object $emoticons
	 * @access public
	 */
	public function addEmoticons(CryoBB_Emoticons $emoticons)
	{
		$this->emoticons = $emoticons;
		return $this;
	}
	
	/**
	 * Converts emoticon text to HTML
	 *
	 * Converts emoticon text to HTML (presumably images) based on the definition
	 * of the emoticons.
	 *
	 * @param string $str The main string of text
	 * @access private
	 */
	private function makeEmoticons($str)
	{
		if (!empty($this->emoticons))
		{
			$func = $this->emoticons->isCaseSensitive ? 'str_replace' : 'str_ireplace';
		
			return $func(
				array_keys($this->emoticons->emoticons),
				array_values($this->emoticons->emoticons),
				$str
			);
		}
		return $str;
	}
	
	/**
	 * Replaces bb code tags with their HTML equivelants
	 *
	 * Does the necessary magic to turn bb code tags, parameters, and contents 
	 * into the desired output based on tag definitions.
	 *
	 * @param string $str The main string of text
	 * @param array $matches - an array of bb code matches found in the seached 
	 *  string
	 * @access private
	 */
	private function makeTags($str, $matches)
	{
		while (isset($matches[0]))
		{
			$match = $matches[0];
			
			$matches = array_slice($matches, 1);
			$replacement = $this->makeTag($str, $match, $matches);
			
			$str = substr_replace(
				$str,
				$replacement,
				$match->offset,
				$match->length
			);
		}
		return $str;
	}
	
	/**
	 * Replaces a simple tag (e.g. [b]text[/b]) with it's HTML counterparts
	 *
	 * @param string $str The main string of text
	 * @param object $match - the tag match from regexp search
	 * @param array $matches - The remaining tags
	 * @access private
	 */
	private function makeTag($str, $match, $matches)
	{
		// Get correct tag object
		$tag = $this->tags[$match->tag];
		
		// Handle ignoring
		if ($match->ignore == true)
			return $match->match;
		$ignoreChildren = !$tag->parseContents;
		
		// Process/parse parameter and content
		if ($tag->parseParameter)
			$match->reparseParameter($this->tags);
		$match->processContent($tag);
		$match->processParameter($tag);
		
		// Get format from Tag object
		$build = $tag->build($match->parameter);
		$replacement = $build[0] . $match->content . $build[1];
		
		// Define offsets for related tags
		$innerOffset = $match->calculateOpenTagDifference($build[0]);
		$outerOffset = strlen($replacement) - $match->length;
		
		// Correct all following matches
		foreach ($matches as $otherMatch)
		{
			if ($otherMatch->inMatch($match))
			{
				$otherMatch->addOffset($innerOffset);
				
				if ($ignoreChildren)
					$otherMatch->setIgnore(true);
			} else
			{
				$otherMatch->addOffset($outerOffset);
			}
		}
		
		// Run replacement
		return $replacement;
	}
	
	/**
	 * Replaces new lines with line breaks
	 *
	 * Replaces new lines with line breaks.  If php version is >= 5.3.0, boolean 
	 * value can be passed to make() and then passed to this method to change 
	 * XHTML style line breaks (<br />) to regular line breaks (<br>)
	 *
	 * @param boolean $isXHTML
	 * @access private
	 * @return void
	 */
	private function makeLineBreaks($str, $isXHTML)
	{
		return (version_compare(PHP_VERSION, '5.3.0') >= 0)
			? nl2br($str, $isXHTML)
			: nl2br($str);
	}
}